tsconfig.json의 include에 프로젝트 루트 경로의 파일을 추가하면 rootDir이 변경될 수 있음 {troubleshooting}

PR 머지 후 갑자기 발생한 도커 배포 문제

아래 에러 로그에 따르면 도커 컨테이너가 /app/dist/main을 찾을 수 없다는데..

racketime-api on  dev [$] via ⬢ v22.11.0 on 🐳 v27.5.1 took 3.7s
➜ docker-compose -f infra/docker-compose.racket-time-api.build.yml up --build
WARN[0000] /Users/choiwheatley/workspace/racketime-api/infra/docker-compose.racket-time-api.build.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Building 39.1s (13/13) FINISHED                                                                                                                             docker:desktop-linux
 => [app internal] load build definition from Dockerfile.prod                                                                                                                   0.0s
 => => transferring dockerfile: 652B                                                                                                                                            0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)                                                                                                  0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 12)                                                                                                 0.0s
 => [app internal] load metadata for docker.io/library/node:lts                                                                                                                 0.9s
 => [app internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 112B                                                                                                                                               0.0s
 => [app internal] load build context                                                                                                                                           0.1s
 => => transferring context: 1.16MB                                                                                                                                             0.1s
 => [app builder 1/4] FROM docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                                                   0.0s
 => => resolve docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                                                               0.0s
 => CACHED [app builder 2/4] WORKDIR /app                                                                                                                                       0.0s
 => [app builder 3/4] COPY . /app                                                                                                                                               0.2s
 => [app builder 4/4] RUN npm i -g pnpm &&     pnpm install &&     npx prisma generate &&     pnpm run build                                                                   20.5s
 => CACHED [app prod 3/5] RUN apt-get update && apt-get install -y curl                                                                                                         0.0s
 => [app prod 4/5] COPY --chown=node:node --from=builder /app /app                                                                                                              2.2s
 => [app prod 5/5] RUN chmod +x /app/infra/entrypoint.sh                                                                                                                        0.3s
 => [app] exporting to image                                                                                                                                                   12.8s
 => => exporting layers                                                                                                                                                         9.8s
 => => exporting manifest sha256:e865d50bb6d0e58795de4cc2f259943fdee1c8c7c844f10a31a65f9c5bacc30b                                                                               0.0s
 => => exporting config sha256:cc2bb129c7424ccd0885f910fc1db13907d34293e98ec2aa175c6952ad30d29e                                                                                 0.0s
 => => exporting attestation manifest sha256:bd6dc726a0cfa611619938f22e2666b9c36cfbac25679306daaa94d838c30b61                                                                   0.0s
 => => exporting manifest list sha256:82cefa417aa2828567d7e9f563b055c7e9d9f001cee598649adbf32e23263d8d                                                                          0.0s
 => => naming to docker.io/library/infra-app:latest                                                                                                                             0.0s
 => => unpacking to docker.io/library/infra-app:latest                                                                                                                          3.0s
 => [app] resolving provenance for metadata file                                                                                                                                0.0s
[+] Running 2/2
 ✔ app                    Built                                                                                                                                                 0.0s
 ✔ Container infra-app-1  Recreated                                                                                                                                             1.0s
Attaching to app-1
app-1  | node:internal/modules/cjs/loader:1228
app-1  |   throw err;
app-1  |   ^
app-1  |
app-1  | Error: Cannot find module '/app/dist/main'
app-1  |     at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
app-1  |     at Function._load (node:internal/modules/cjs/loader:1055:27)
app-1  |     at TracingChannel.traceSync (node:diagnostics_channel:322:14)
app-1  |     at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
app-1  |     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
app-1  |     at node:internal/main/run_main_module:36:49 {
app-1  |   code: 'MODULE_NOT_FOUND',
app-1  |   requireStack: []
app-1  | }
app-1  |
app-1  | Node.js v22.14.0
app-1 exited with code 1

main 브랜치에서 똑같은 걸 하면..

잘 된다. 이건 머지하면서 뭐가 잘못 꼬인거다.

~/Downloads/app-b8fc19-250225_015236436 via ⬢ v22.11.0 on 🐳 v27.5.1
➜ docker-compose -f infra/docker-compose.racket-time-api.build.yml up --build
WARN[0000] /Users/choiwheatley/Downloads/app-b8fc19-250225_015236436/infra/docker-compose.racket-time-api.build.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Building 40.2s (14/14) FINISHED                                                                                                       docker:desktop-linux
 => [app internal] load build definition from Dockerfile.prod                                                                                             0.0s
 => => transferring dockerfile: 698B                                                                                                                      0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)                                                                            0.0s
 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 12)                                                                           0.0s
 => [app internal] load metadata for docker.io/library/node:lts                                                                                           1.8s
 => [app auth] library/node:pull token for registry-1.docker.io                                                                                           0.0s
 => [app internal] load .dockerignore                                                                                                                     0.0s
 => => transferring context: 158B                                                                                                                         0.0s
 => [app internal] load build context                                                                                                                     0.1s
 => => transferring context: 239.31kB                                                                                                                     0.0s
 => [app builder 1/4] FROM docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                             0.0s
 => => resolve docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95                                         0.0s
 => CACHED [app builder 2/4] WORKDIR /app                                                                                                                 0.0s
 => [app builder 3/4] COPY . /app                                                                                                                         0.1s
 => [app builder 4/4] RUN npm i -g pnpm &&     pnpm install &&     npx prisma generate &&     pnpm run build                                             22.6s
 => CACHED [app prod 3/5] RUN apt-get update && apt-get install -y curl                                                                                   0.0s
 => [app prod 4/5] COPY --chown=node:node --from=builder /app /app                                                                                        1.7s
 => [app prod 5/5] RUN chmod +x /app/infra/entrypoint.sh                                                                                                  0.3s
 => [app] exporting to image                                                                                                                             11.8s
 => => exporting layers                                                                                                                                   9.0s
 => => exporting manifest sha256:33099e4489f7619905bd1e24cb3549dc57718342128016c9b4bfdf6bccfe6a6e                                                         0.0s
 => => exporting config sha256:bebde6a9a02c95499840535f932d05fb0240be070bc33a27866062232662af3e                                                           0.0s
 => => exporting attestation manifest sha256:4f8247d773818dc9ab6ce5364dec9d6c2ea080ebf53d6f6640ec62baa12fb95f                                             0.0s
 => => exporting manifest list sha256:dea7e14826f30de52ceff975b5b3fcf95608913036016733d4dfcc0036210eda                                                    0.0s
 => => naming to docker.io/library/infra-app:latest                                                                                                       0.0s
 => => unpacking to docker.io/library/infra-app:latest                                                                                                    2.9s
 => [app] resolving provenance for metadata file                                                                                                          0.0s
[+] Running 2/2
 ✔ app                    Built                                                                                                                           0.0s
 ✔ Container infra-app-1  Recreated                                                                                                                       1.5s
Attaching to app-1
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [NestFactory] Starting Nest application...
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] VerificationModule dependencies initialized +16ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +0ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] ProductLogModule dependencies initialized +0ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] ConfigModule dependencies initialized +1ms
app-1  | [Nest] 7  - 03/13/2025, 7:43:09 AM     LOG [InstanceLoader] AppModule dependencies initialized +0ms

main, dev 빌드 이후 dist 디렉터리 내용물도 차이가 크다.

dev

얘는 src 안에 main.js가 들어갔는데? 어찌 된 일이지?

racketime-api/dist
➜ l
total 1056
drwxr-xr-x@  7 choiwheatley  staff     224 Mar 13 16:36 ./
drwxr-xr-x@ 31 choiwheatley  staff     992 Mar 13 16:43 ../
-rw-r--r--@  1 choiwheatley  staff     108 Mar 13 16:36 jest.config.d.ts
-rw-r--r--@  1 choiwheatley  staff     676 Mar 13 16:36 jest.config.js
-rw-r--r--@  1 choiwheatley  staff     515 Mar 13 16:36 jest.config.js.map
drwxr-xr-x@ 45 choiwheatley  staff    1440 Mar 13 16:36 src/
-rw-r--r--@  1 choiwheatley  staff  527286 Mar 13 16:36 tsconfig.build.tsbuildinfo

➜ ls src
academy               app.module.d.ts       coach-file            gamedatalog           metrix                reservation-code      tag                   verification
academy-bot-scheduler app.module.js         constant.d.ts         guidance              order                 settlement            tennis-content
academy-coach         app.module.js.map     constant.js           health                payment               shared                ticket
academy-tag           auth                  constant.js.map       main.d.ts             product-log           sita                  trainer-ota
admin                 business-info         court                 main.js               product-order         staff                 types
advertisement         coach-code            fb                    main.js.map           reservation           switch-bot            user

main

Downloads/app-b8fc19-250225_015236436/dist
➜ l
total 1104
drwxr-xr-x@ 47 choiwheatley  staff    1504 Mar 13 16:38 ./
drwx------@ 26 choiwheatley  staff     832 Mar 13 16:40 ../
drwxr-xr-x@ 19 choiwheatley  staff     608 Mar 13 16:38 academy/
...
-rw-r--r--@  1 choiwheatley  staff    3008 Mar 13 16:38 main.js
...
drwxr-xr-x@ 15 choiwheatley  staff     480 Mar 13 16:38 user/
drwxr-xr-x@  9 choiwheatley  staff     288 Mar 13 16:38 verification/

dev는 여기에 main.js 파일이 빠졌던 것이고 실행을 못했던 것이다. 다른 파일들도 하나도 안 들어가 있는 걸 보니 높은 확률로 빌드 실패가 뜬 것 같다. 어째서 빌드가 실패했는데 다음 스텝으로 넘어간 거지?

package.json 변경이력

아래처럼 해놨다. 내가 로컬에서 pnpm run build 하고 pnpm run start:prod가 자꾸 안되길래 저렇게 바꿔놓은 채로 실행한 거다. 빌드 결과는 손도 안 댔는데 언제부터 빌드위치가 dist/src로 바뀐거지?

- "start:prod": "node dist/main",
+ "start:prod": "node dist/src/main.js",

(CONCLUSION) tsconfig.json 쪽에 include 문제였다.

- "include": ["src/**/*.ts", "src/uploads/template.xlsx"]
+ "include": ["src/**/*.ts", "src/uploads/template.xlsx", "jest.config.ts"]

이걸 한 뒤로 dist 폴더구조가 바뀐 것이고 "jest.config.ts" 아이템을 제거하자 다시 dist 폴더 안에 컴파일 결과가 들어왔다. 원인이 뭐지?

🔍 원인 분석: includejest.config.ts 추가가 dist 구조를 바꾼 이유

결론:
tsconfig.json에서 include"jest.config.ts"를 추가했을 때, **TypeScript의 루트 디렉터리 감지(rootDir 설정)**가 달라졌기 때문이야.


💡 원리: rootDir 결정 방식

TypeScript는 tsconfig.json에서 include된 파일들의 공통 상위 디렉터리rootDir로 설정하고, outDir로 변환할 때 rootDir을 기준으로 상대 경로를 유지해.


⚠️ jest.config.ts 추가가 왜 문제였나?

  1. 원래는 include"src/**/*.ts"였기 때문에 TypeScript는 src/rootDir로 인식하고 dist/ 안에 main.js 등 관련 파일을 넣었음.

    src/         ->  dist/
    ├── main.ts  ->  ├── main.js
    ├── user/    ->  ├── user/
    ├── academy/ ->  ├── academy/
    
  2. 하지만 jest.config.tssrc/가 아닌 **루트 경로(/)**에 있었을 가능성이 높음.

    📁 프로젝트 루트
    ├── src/
    │   ├── main.ts
    │   ├── user/
    │   ├── academy/
    ├── jest.config.ts   # ✅ include에 추가됨!
    
  3. TypeScript가 include된 모든 파일의 공통 상위 경로를 rootDir로 설정하는데,

    • src/**/*.ts만 포함되었을 때 → rootDirsrc/
    • jest.config.ts를 포함했을 때 → rootDir프로젝트 루트(/)
  4. rootDir/로 확장되면서 TypeScript는 다음처럼 파일 구조를 유지하려고 함:

    📁 dist/
    ├── src/           ✅ 원래 dist 안에 없던 `src/` 폴더가 추가됨
    │   ├── main.js
    │   ├── user/
    │   ├── academy/
    ├── jest.config.js
    

    이 때문에 dist/main.js가 없어지고 dist/src/main.js가 생긴 것!


🛠 해결 방법

해결책 1: jest.config.tsinclude에서 제거 (지금 해결한 방법)
해결책 2: tsconfig.jsonrootDir을 명시적으로 설정

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

해결책 3: excludejest.config.ts 추가

{
  "exclude": ["jest.config.ts"]
}

📌 결론

tsconfig.jsoninclude에 프로젝트 루트 경로의 파일을 추가하면 TypeScript의 rootDir이 변경될 수 있음.
rootDir이 변경되면 빌드 결과의 dist/ 폴더 구조가 예상과 달라짐.
✅ 해결 방법: rootDir을 명시적으로 src/로 설정하거나 jest.config.tsinclude에서 제거. 🚀